/*
 * Copyright (c) 2005, 2014 vacoor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */
package org.ponly.uno4j.core;

import com.sun.star.lib.uno.helper.UnoUrl;
import org.ponly.common.proc.Proc;
import org.ponly.common.util.LazyValue;
import org.ponly.common.util.StringUtils;
import org.ponly.uno4j.util.UnoUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.regex.Pattern;

import static org.ponly.uno4j.util.UnoUtils.*;

/**
 * LibreOffice/OpenOffice 进程
 * <p>
 * OpenOffice 进程说明:<br>
 * 警告:
 * 1. 对于同一个 pipe, socket 服务进程只能进行串行处理(不能同时建立多个 Bridge或同一Bridge同时处理多个任务, 同一 pipe/socket 进程只能建立一个bridge处理)<br>
 * 2. 对于同一工作空间只能串行处理(默认情况下,服务进程会在同一工作空间进行处理, 可以通过 -env:UserInstallation=file:///path/指定不同工作空间)<br>
 * 3. PIPE 方式通过 name 来识别不同的 PIPE Bridge, 因此不同的 pipe bridge 应该有不同的名称
 * 因此，每个 soffice 进程应该有唯一的工作空间且同时只能建立一个bridge进行处理
 * <p>
 * 启用多个 soffice 进程:<br>
 * 如: soffice -nologo -nodefault -norestore -nocrashreport -nolockcheck -invisible -headless -nofirststartwizard -accept=socket,host=localhost,port=9080;urp; -env:UserInstallation=file:///tmp/soffice1
 * <p>
 *
 * @author vacoor
 * @see com.sun.star.comp.helper.Bootstrap#bootstrap
 */
public class OOoProcess {
    private static final Logger LOG = LoggerFactory.getLogger(OOoProcess.class);
    private static final String TMPDIR = System.getProperty("java.io.tmpdir");

    private String ooHome;                  // LibreOffice/OpenOffice Home
    private int[] pids;                     // OOo 进程ID, windows 下会有多个进程
    private Process process;                // OOo 进程对象
    private final UnoUrl unoUrl;

    /**
     * @param unoUrl UNO Url String
     */
    public OOoProcess(String unoUrl) {
        this(parseUnoUrl(unoUrl));
    }

    /**
     * @param unoUrl UNO Url
     */
    public OOoProcess(UnoUrl unoUrl) {
        this.unoUrl = unoUrl;
    }

    public synchronized OOoProcess start() {
        if (isAlive()) {
            return this;
        }

        stopInternal();
        /*-
         * 查找已经存在的服务(soffice.exe/soffice.bin), windows 下会有多个进程
         */
        String conn = unoUrl.getConnectionAndParametersAsString();
        Iterator<Proc> soffice = Proc.query(".*soffice.*" + Pattern.quote(conn) + ".*");
        Set<Integer> processIds = new HashSet<Integer>();
        while (soffice.hasNext()) {
            Proc proc = soffice.next();
            processIds.add(proc.getPid());

            LOG.warn("发现与将要启动的OOoProcess相同服务已启动, 使用该进程, pid: {}, commandLine: {}", proc.getPid(), proc.getCommand());
        }

        // 记录进程 pid
        pids = new int[processIds.size()];
        int i = 0;
        for (Integer pid : processIds) {
            pids[i++] = pid;
        }

        // 不存在已经启动的进程
        if (1 > pids.length) {
            startOOoProcess();
        }
        return this;
    }

    public synchronized void restart() {
        LOG.info("Restart OOoProcess: {}", join(",", pids));

        stop();
        start();
    }

    public synchronized void stop() {
        LOG.info("Stop OOoProcess: {}", join(",", pids));
        try {
            stopInternal();
        } finally {
            cleanup();
        }
    }

    protected void stopInternal() {
        try {
            if (null != process) {
                process.destroy();
            }
            if (null != pids) {
                for (int pid : pids) {
                    try {
                        if (pid != -1) {
                            Proc.kill(pid);
                        }
                    } catch (Exception ex) {
                        // ignore
                        LOG.warn("Kill OOoProcess failed.", ex);
                    }
                }
            }
        } finally {
            pids = new int[0];
            process = null;
        }
    }

    /**
     * 当前 OOo 进程是否依然存活
     *
     * @return OOo 是否存活
     */
    public synchronized boolean isAlive() {
        boolean alive = false;
        try {
            // 如果是当前应用启动, 则通过 process 判断
            if (null != process) {
                process.exitValue();    // 如果还在运行, 这里会抛出异常
            } else if (Proc.hasAny(pids)) {
                alive = true;
            }
        } catch (Exception ignore) {
            alive = true;
        }
        return alive;
    }

    /**
     * 清理当前 OOo 进程的工作空间
     */
    protected void cleanup() {
        String workspace = getUserInstallation(unoUrl.getConnectionAndParametersAsString());
        try {
            workspace = new URL(workspace).getFile();
        } catch (MalformedURLException e) {
            // ignore
        }
        LOG.info("Cleanup workspace: {}", workspace);

        deleteRecursively(new File(workspace));
    }

    private void deleteRecursively(File f) {
        if (f.exists() && f.isFile() && f.canWrite()) {
            f.delete();
        } else if (f.exists() && f.isDirectory() && f.canRead()) {
            File[] files = f.listFiles();
            files = null != files ? files : new File[0];

            for (File file : files) {
                deleteRecursively(file);
            }
            if (f.canWrite()) {
                f.delete();
            }
        }
    }

    private String getUserInstallation(String unoUrl) {
        String path = "soffice_uno4j/" + unoUrl.replace(',', '_').replace('=', '-') + "@" + Proc.getCurrentProcessId();
        return toUrl(new File(TMPDIR, path));
    }

    /**
     * 启动 OpenOffice 进程
     */
    private void startOOoProcess() {
        ooHome = StringUtils.hasText(ooHome) ? ooHome : OO_HOME.getValue();

        String soffice = getSoffice(ooHome);
        if (null == soffice) {
            throw new IllegalStateException("Can't found LibreOffice/OpenOffice(soffice) executable");
        }

        String conn = unoUrl.getConnectionAndParametersAsString();
        String protocol = unoUrl.getProtocolAndParametersAsString();
        String[] command = new String[]{
                soffice,
                // 参数应该使用 "--", 为了兼容这里还用 "-"
                "-accept=" + conn + ";" + protocol + ";",
                // 如果要启动多个进程需要 UserInstallation
                "-env:UserInstallation=" + getUserInstallation(conn),
                "-nologo",
                "-nodefault",
                "-norestore",
                "-nocrashreport",
                "-nolockcheck",
                "-invisible",
                "-headless",
                "-nofirststartwizard"/*,
                "-accept=" + unoUrl + ";" + bridgeType + ";",
                // 如果要启动多个进程需要 UserInstallation
                "-env:UserInstallation=" + getUserInstallation(unoUrl)*/
        };

        try {
            if (LOG.isInfoEnabled()) {
                LOG.info("Start OOoProcess: {}", join(" ", command));
            }

            // process = new Command(command).start();
            process = Runtime.getRuntime().exec(command);
            try {
                // windows 这里拿到的是 soffice.exe 进程号
                pids = new int[]{Proc.getProcessId(process)};
            } catch (Exception e) {
                LOG.error("Get Process ID Exception: {}", e);
            }

            try {
                // 如果启动后立即就退出了
                int ret = process.exitValue();
                LOG.error("Start OOoProcess failed, pid: {}, returnCode: {}, command: {}", join(",", pids), ret, join(" ", command));
                throw new IllegalStateException();
            } catch (Exception ignore) {
                // ignore
            }

            if (LOG.isInfoEnabled()) {
                LOG.info("Start OOoProcess successful, pid: {}, commandLine: {}", join(",", pids), join(" ", command));
            }
        } catch (IOException e) {
            throw new IllegalStateException("Start OOoProcess(soffice) failed", e);
        }
    }

    private static final LazyValue<String> OO_HOME = new LazyValue<String>() {
        @Override
        protected String compute() {
            return UnoUtils.findOOHome();
        }
    };
}
